package com.unnet.yjs.controller.api.v1;

import com.unnet.yjs.annotation.HttpMethod;
import com.unnet.yjs.base.ContainerProperties;
import com.unnet.yjs.entity.OssFileRecord;
import com.unnet.yjs.service.OssFileRecordService;
import com.unnet.yjs.util.FileUtil;
import com.unnet.yjs.util.IOssOperation;
import com.xiaoleilu.hutool.util.StrUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.catalina.connector.ClientAbortException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * Email: love1208tt@foxmail.com
 * Copyright (c)  2019. missbe
 *
 * @author lyg   19-7-29 下午9:21
 **/
@RestController
@Api(tags = "OssStreamConvertController", description = "文件上传控制器")
@RequestMapping("/api/v1/resource/file/")
public class OssStreamConvertController {
    private static final Logger LOGGER = LoggerFactory.getLogger(OssStreamConvertController.class);
    @Resource
    private ContainerProperties containerProperties;

    @Autowired
    @Qualifier("paasOssTool")
    private IOssOperation paasOssTool;
    @Autowired
    @Qualifier("minIoOssTool")
    private IOssOperation minIoOssTool;

    @Resource
    private OssFileRecordService ossFileRecordService;


    /**
     * 对象存储中转请求链接-根据文件名字请求对象存储的文件流
     *
     * @param fileName 文件名称
     */
    @GetMapping("video/{fileName}")
    @ApiOperation(value = "对象存储文件流中转接口", httpMethod = HttpMethod.GET)
    @ApiImplicitParams({@ApiImplicitParam(name = "fileName", value = "文件名称", paramType = "path")})
    public void videoPlayer(@PathVariable(value = "fileName") String fileName, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (minIoOssTool == null && "myOss".equalsIgnoreCase(containerProperties.getFileUploadType())) {
            OutputStream out = response.getOutputStream();
            out.write(("OSS文件服务器配置出现问题,请修复后重试.").getBytes(StandardCharsets.ISO_8859_1));
            out.flush();
            out.close();
            return;
        } else if (paasOssTool == null && "paas".equalsIgnoreCase(containerProperties.getFileUploadType())) {
            OutputStream out = response.getOutputStream();
            out.write(("OSS文件服务器配置出现问题,请修复后重试.").getBytes(StandardCharsets.ISO_8859_1));
            out.flush();
            out.close();
            return;
        }

        ///是否开启本地缓存视频mp4文件
        String filePath = containerProperties.getFileCacheLocation() + fileName;
        File file = new File(filePath);
        File parentDir = file.getParentFile();
        if (!parentDir.exists()) {
            boolean isMakeParentDir = parentDir.mkdirs();
            if (isMakeParentDir) {
                LOGGER.info("创建文件夹{}成功.", parentDir.getAbsolutePath());
            } else {
                LOGGER.error("创建文件夹{}失败.", parentDir.getAbsolutePath());
            }
        }//end if
        if (!file.exists() || file.length() == 0) {
            boolean isDelete = file.delete();
            LOGGER.error("文件：{}，长度为：{},删除标志：{}重新从对象存储拉取", fileName, FileUtil.formatFileSize(file.length()), isDelete);
            ////缓存文件到本地
            if (!cacheFile2Local(fileName, file)) {
                OutputStream out = response.getOutputStream();
                out.write("对象存储加载文件失败".getBytes());
                out.flush();
                out.close();
            }///end if

        } ///end if
        LOGGER.info("【Video】文件：{}，总长度：{}", file.getName(), FileUtil.formatFileSize(file.length()));
        ///对文件执行分块
        fileChunkDownload(filePath, request, response);
        /////添加文件访问记录
        OssFileRecord ossFileRecord = ossFileRecordService.findByFileName(fileName);
        if (Objects.nonNull(ossFileRecord)) {
            ossFileRecord.setFileLength(String.valueOf(file.length()));
            ossFileRecord.setVisitCount(ossFileRecord.getVisitCount() + 1);
        } else {
            ossFileRecord = new OssFileRecord();
            ossFileRecord.setFileName(fileName);
            ossFileRecord.setFileLength(String.valueOf(file.length()));
            ossFileRecord.setVisitCount(1);
            ossFileRecord.setRemarks("OssFileRecord");
        }
        ossFileRecordService.insertOrUpdate(ossFileRecord);
    }

    /**
     * 文件支持分块下载和断点续传
     *
     * @param filePath 文件完整路径
     * @param request  请求
     * @param response 响应
     */
    private void fileChunkDownload(String filePath, HttpServletRequest request, HttpServletResponse response) {
        String range = request.getHeader("Range");
        LOGGER.info("current request rang:" + range);
        File file = new File(filePath);
        //开始下载位置
        long startByte = 0;
        //结束下载位置
        long endByte = file.length() - 1;
        LOGGER.info("文件开始位置：{}，文件结束位置：{}，文件总长度：{}", startByte, endByte, file.length());

        //有range的话
        if (range != null && range.contains("bytes=") && range.contains("-")) {
            range = range.substring(range.lastIndexOf("=") + 1).trim();
            String[] ranges = range.split("-");
            try {
                //判断range的类型
                if (ranges.length == 1) {
                    //类型一：bytes=-2343
                    if (range.startsWith("-")) {
                        endByte = Long.parseLong(ranges[0]);
                    }
                    //类型二：bytes=2343-
                    else if (range.endsWith("-")) {
                        startByte = Long.parseLong(ranges[0]);
                    }
                }
                //类型三：bytes=22-2343
                else if (ranges.length == 2) {
                    startByte = Long.parseLong(ranges[0]);
                    endByte = Long.parseLong(ranges[1]);
                }

            } catch (NumberFormatException e) {
                startByte = 0;
                endByte = file.length() - 1;
                LOGGER.error("Range Occur Error,Message:{}", e.getLocalizedMessage());
            }
        }

        //要下载的长度（为啥要加一问小学数学老师去）
        long contentLength = endByte - startByte + 1;
        //文件名
        String fileName = file.getName();
        //文件类型
        String contentType = request.getServletContext().getMimeType(fileName);
        ////解决下载文件时文件名乱码问题
        byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
        String downloadFileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);


        //各种响应头设置
        //参考资料：https://www.ibm.com/developerworks/cn/java/joy-down/index.html
        //坑爹地方一：看代码
        response.setHeader("Accept-Ranges", "bytes");
        //坑爹地方二：http状态码要为206
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        response.setContentType(contentType);
        response.setHeader("Content-Type", contentType);
        //这里文件名换你想要的，inline表示浏览器直接实用（我方便测试用的）
        //参考资料：http://hw1287789687.iteye.com/blog/2188500
        response.setHeader("Content-Disposition", "attachment;filename=" + downloadFileName);
        response.setHeader("Content-Length", String.valueOf(contentLength));
        //坑爹地方三：Content-Range，格式为
        // [要下载的开始位置]-[结束位置]/[文件总大小]
        response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());


        BufferedOutputStream outputStream;
        RandomAccessFile randomAccessFile = null;
        //已传送数据大小
        long transmitted = 0;
        try {
            randomAccessFile = new RandomAccessFile(file, "r");
            outputStream = new BufferedOutputStream(response.getOutputStream());
            byte[] buff = new byte[4096];
            int len = 0;
            randomAccessFile.seek(startByte);
            //坑爹地方四：判断是否到了最后不足4096（buff的length）个byte这个逻辑（(transmitted + len) <= contentLength）要放前面！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！！
            //不然会会先读取randomAccessFile，造成后面读取位置出错，找了一天才发现问题所在
            while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
                outputStream.write(buff, 0, len);
                transmitted += len;
                //                //停一下，方便测试，用的时候删了就行了
                //                Thread.sleep(10);
            }
            //处理不足buff.length部分
            if (transmitted < contentLength) {
                len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
                outputStream.write(buff, 0, len);
                transmitted += len;
            }

            outputStream.flush();
            response.flushBuffer();
            randomAccessFile.close();
            LOGGER.info("下载完毕：" + startByte + "-" + endByte + "：" + transmitted);
        } catch (ClientAbortException e) {
            LOGGER.warn("用户停止下载：" + startByte + "-" + endByte + "：" + transmitted);
            //捕获此异常表示拥护停止下载
        } catch (IOException e) {
            e.printStackTrace();
            LOGGER.error("用户下载IO异常，Message：{}", e.getLocalizedMessage());
        } finally {
            try {
                if (randomAccessFile != null) {
                    randomAccessFile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }///end try
    }

    /**
     * 对象存储中转请求链接-根据文件名字请求对象存储的文件流
     *
     * @param fileName 文件名称
     */
    @GetMapping("storage/{fileName}")
    @ApiOperation(value = "对象存储文件流中转接口", httpMethod = HttpMethod.GET)
    @ApiImplicitParams({@ApiImplicitParam(name = "fileName", value = "文件名称", paramType = "path")})
    public ResponseEntity<byte[]> storage(@PathVariable(value = "fileName") String fileName, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if (minIoOssTool == null && "myOss".equalsIgnoreCase(containerProperties.getFileUploadType())) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("OSS文件服务器配置出现问题,请修复后重试.".getBytes(StandardCharsets.UTF_8));
        } else if (paasOssTool == null && "paas".equalsIgnoreCase(containerProperties.getFileUploadType())) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("OSS文件服务器配置出现问题,请修复后重试.".getBytes(StandardCharsets.UTF_8));
        }
        String filePath = containerProperties.getFileCacheLocation() + fileName;
        File file = new File(filePath);
        File parentDir = file.getParentFile();
        if (!parentDir.exists()) {
            boolean isMakeParentDir = parentDir.mkdirs();
            if (isMakeParentDir) {
                LOGGER.info("创建文件夹{}成功.", parentDir.getAbsolutePath());
            } else {
                LOGGER.error("创建文件夹{}失败.", parentDir.getAbsolutePath());
            }
        }//end if
        if (!file.exists() || file.length() == 0) {
            boolean isDelete = file.delete();
            LOGGER.error("文件：{}，长度为：{},删除标志：{}重新从对象存储拉取", fileName, FileUtil.formatFileSize(file.length()), isDelete);
            ////缓存文件到本地
            if (!cacheFile2Local(fileName, file)) {
                OutputStream out = response.getOutputStream();
                out.write("对象存储加载文件失败".getBytes());
                out.flush();
                out.close();
            }///end if

        } ///end if
        LOGGER.info("【普通】文件：{}，总大小：{}", file.getName(), FileUtil.formatFileSize(file.length()));

        String contentType = request.getServletContext().getMimeType(fileName);
        ////解决下载文件时文件名乱码问题
        byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
        String downloadFileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        InputStream is = new FileInputStream(new File(filePath));
        IOUtils.copy(is, baos);

        byte[] bytes = baos.toByteArray();
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        InputStreamResource resource = new InputStreamResource(bais);
        long contentLength = resource.contentLength();

        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.valueOf(contentType));
        headers.setContentDispositionFormData("attachment", downloadFileName);
        headers.setContentLength(contentLength);

        /////添加文件访问记录
        OssFileRecord ossFileRecord = ossFileRecordService.findByFileName(fileName);
        if (Objects.nonNull(ossFileRecord)) {
            ossFileRecord.setFileLength(String.valueOf(file.length()));
            ossFileRecord.setVisitCount(ossFileRecord.getVisitCount() + 1);
        } else {
            ossFileRecord = new OssFileRecord();
            ossFileRecord.setFileName(fileName);
            ossFileRecord.setFileLength(String.valueOf(file.length()));
            ossFileRecord.setVisitCount(1);
            ossFileRecord.setRemarks("OssFileRecord");
        }
        ossFileRecordService.insertOrUpdate(ossFileRecord);


        return ResponseEntity.ok().headers(headers).body(bytes);
    }

    /**
     * 缓存文件到本地
     *
     * @param fileName 文件名
     * @param file     文件对象
     */
    private boolean cacheFile2Local(String fileName, File file) {
        ///本地文件不存在,从OSS下载到本地
        boolean isMakeNewFile;
        try {
            isMakeNewFile = file.createNewFile();
        } catch (IOException e) {
            LOGGER.error("创建文件：{}错误,msg:{}", fileName, e.getLocalizedMessage());
            return false;
        }
        if (isMakeNewFile) {
            LOGGER.info("创建文件{}成功.", file.getAbsolutePath());
        } else {
            LOGGER.error("创建文件{}失败.", file.getAbsolutePath());
        }
        InputStream is = null;
        try {
            if (StrUtil.equalsIgnoreCase(containerProperties.getFileUploadType(), "myOss")) {
                is = minIoOssTool.load(fileName);
            }
            if (StrUtil.equalsIgnoreCase(containerProperties.getFileUploadType(), "paas")) {
                is = paasOssTool.load(fileName);
            }
        } catch (Exception e) {
            e.printStackTrace();
            e.printStackTrace();
            LOGGER.error("对象存储加载文件失败,msg:" + e.getLocalizedMessage());
            return false;
        }
        ////判断流不为空
        Objects.requireNonNull(is);

        BufferedOutputStream bos;
        try {
            bos = new BufferedOutputStream(new FileOutputStream(file));
            byte[] buffer = new byte[4096];
            int length;
            while ((length = is.read(buffer)) != -1) {
                bos.write(buffer, 0, length);
            }
            bos.flush();
            bos.close();
            is.close();
        } catch (IOException e) {
            LOGGER.error("文件：{}写入字节IO错误,msg:{}", fileName, e.getLocalizedMessage());
            return false;
        }
        return true;
    }
}
